package query

import (
	"encoding/json"
	"fmt"

	"github.com/romberli/das/config"
	"github.com/romberli/das/internal/app/metadata"
	"github.com/romberli/das/internal/dependency/query"
	"github.com/romberli/das/pkg/message"
	msgquery "github.com/romberli/das/pkg/message/query"
	"github.com/romberli/das/pkg/util/deployment"
	utilquery "github.com/romberli/das/pkg/util/query"
	"github.com/romberli/go-util/constant"
	"github.com/romberli/go-util/middleware/clickhouse"
	"github.com/romberli/go-util/middleware/mysql"
	"github.com/romberli/log"
	"github.com/spf13/viper"
)

const (
	queryQueriesJSON = "queries"

	pmmDBName = "pmm"
)

var _ query.Query = (*Query)(nil)

// Query include several members of a query
type Query struct {
	SQLID           string  `middleware:"sql_id" json:"sql_id"`
	Fingerprint     string  `middleware:"fingerprint" json:"fingerprint"`
	Example         string  `middleware:"example" json:"example"`
	DBName          string  `middleware:"db_name" json:"db_name"`
	ExecCount       int     `middleware:"exec_count" json:"exec_count"`
	TotalExecTime   float64 `middleware:"total_exec_time" json:"total_exec_time"`
	AvgExecTime     float64 `middleware:"avg_exec_time" json:"avg_exec_time"`
	RowsExaminedMax int     `middleware:"rows_examined_max" json:"rows_examined_max"`
}

// NewEmptyQuery return query.Query
func NewEmptyQuery() query.Query {
	return &Query{}
}

// GetSQLID returns the sql identity
func (q *Query) GetSQLID() string {
	return q.SQLID
}

// GetFingerprint returns the fingerprint
func (q *Query) GetFingerprint() string {
	return q.Fingerprint
}

// GetExample returns the example
func (q *Query) GetExample() string {
	return q.Example
}

// GetDBName returns the db name
func (q *Query) GetDBName() string {
	return q.DBName
}

// GetExecCount returns the execution count
func (q *Query) GetExecCount() int {
	return q.ExecCount
}

// GetTotalExecTime returns the total execution time
func (q *Query) GetTotalExecTime() float64 {
	return q.TotalExecTime
}

// GetAvgExecTime returns the average execution time
func (q *Query) GetAvgExecTime() float64 {
	return q.AvgExecTime
}

// GetRowsExaminedMax returns the maximum row examined
func (q *Query) GetRowsExaminedMax() int {
	return q.RowsExaminedMax
}

// SetDBName sets db name to the query
func (q *Query) SetDBName(dbName string) {
	q.DBName = dbName
}

// Querier include config of query and connection pool of DAS repo
type Querier struct {
	config  query.Config
	dasRepo query.DASRepo
}

// NewQuerier return *Querier
func NewQuerier(config query.Config, dasRepo query.DASRepo) *Querier {
	return newQuerier(config, dasRepo)
}

// NewQuerierWithGlobal return *Querier with global DASRepo
func NewQuerierWithGlobal(config query.Config) *Querier {
	return newQuerier(config, NewDASRepoWithGlobal())
}

func newQuerier(config query.Config, dasRepo query.DASRepo) *Querier {
	return &Querier{
		config:  config,
		dasRepo: dasRepo,
	}
}

func (q *Querier) getConfig() query.Config {
	return q.config
}

// GetByMySQLClusterIDWithHTTP get queries by mysql cluster id
func (q *Querier) GetByMySQLClusterIDWithHTTP(mysqlClusterID int) ([]query.Query, error) {
	mysqlServerService := metadata.NewMySQLServerServiceWithDefault()
	err := mysqlServerService.GetByClusterID(mysqlClusterID)
	if err != nil {
		return nil, err
	}

	var queries []query.Query
	for _, mysqlServer := range mysqlServerService.GetMySQLServers() {
		qs, err := q.GetByMySQLServerIDWithHTTP(mysqlServer.Identity())
		if err != nil {
			return nil, err
		}

		queries = append(queries, qs...)
	}

	return queries, nil
}

// GetByMySQLServerIDWithHTTP get queries by mysql server id
func (q *Querier) GetByMySQLServerIDWithHTTP(mysqlServerID int) ([]query.Query, error) {
	mysqlServerService := metadata.NewMySQLServerServiceWithDefault()
	err := mysqlServerService.GetByID(mysqlServerID)
	if err != nil {
		return nil, err
	}
	mysqlServer := mysqlServerService.GetMySQLServers()[constant.ZeroInt]
	monitorSystem, err := mysqlServer.GetMonitorSystem()
	if err != nil {
		return nil, err
	}

	return q.getByMySQLServerAndMonitorSystemInfoWithHTTP(
		mysqlServer.GetHostIP(), mysqlServer.GetPortNum(), mysqlServer.GetServiceName(),
		monitorSystem.GetSystemType(), monitorSystem.GetHostIP(), monitorSystem.GetPortNumSlow())
}

// GetByMySQLServerAndMonitorSystemInfoWithHTTP gets slow queries by mysql server and monitor system info with http
func (q *Querier) GetByMySQLServerAndMonitorSystemInfoWithHTTP(
	mysqlServerHostIP string, mysqlServerPortNum int, mysqlServerServiceName string,
	monitorSystemSystemType int, monitorSystemHostIP string, monitorSystemPortNumSlow int) ([]query.Query, error) {
	return q.getByMySQLServerAndMonitorSystemInfoWithHTTP(
		mysqlServerHostIP, mysqlServerPortNum, mysqlServerServiceName,
		monitorSystemSystemType, monitorSystemHostIP, monitorSystemPortNumSlow)
}

// getByMySQLServerAndMonitorSystemInfoWithHTTP gets slow queries by mysql server and monitor system info with http
func (q *Querier) getByMySQLServerAndMonitorSystemInfoWithHTTP(
	mysqlServerHostIP string, mysqlServerPortNum int, mysqlServerServiceName string,
	monitorSystemSystemType int, monitorSystemHostIP string, monitorSystemPortNumSlow int) ([]query.Query, error) {
	if viper.GetString(config.RoleKey) == config.ServerRole {
		// get mysql server
		mysqlService := metadata.NewMySQLServerServiceWithDefault()
		err := mysqlService.GetByHostInfo(mysqlServerHostIP, mysqlServerPortNum)
		if err != nil {
			return nil, err
		}
		// check accessibility
		deploymentService := deployment.NewServiceWithDefault(mysqlService.GetMySQLServers()[constant.ZeroInt])
		if !deploymentService.CheckAccessibility() {
			// prepare data
			data := []byte(fmt.Sprintf(utilquery.MySQLServerAndMonitorSystemInfoHTTPBodyTemplate, mysqlServerHostIP, mysqlServerPortNum, mysqlServerServiceName,
				monitorSystemSystemType, monitorSystemHostIP, monitorSystemPortNumSlow,
				q.getConfig().GetStartTime().Format(constant.TimeLayoutSecond), q.getConfig().GetEndTime().Format(constant.TimeLayoutSecond),
				q.getConfig().GetLimit(), q.getConfig().GetOffset(), q.getConfig().GetLoginName()))
			// send request
			respBody, err := deploymentService.PostDAS(utilquery.GetByMySQLServerAndMonitorSystemInfoPath, data)
			if err != nil {
				return nil, err
			}
			// unmarshal
			var result map[string][]*Query

			err = json.Unmarshal(respBody, &result)
			if err != nil {
				return nil, err
			}
			// prepare result
			var queries []query.Query
			for _, q := range result[queryQueriesJSON] {
				queries = append(queries, q)
			}

			return queries, nil
		}
	}

	return q.getByServiceNameAndMonitorSystemInfoLocal(mysqlServerServiceName, monitorSystemSystemType, monitorSystemHostIP, monitorSystemPortNumSlow)
}

// getByServiceNameAndMonitorSystemInfoLocal gets slow queries by mysql server service name and monitor system info
func (q *Querier) getByServiceNameAndMonitorSystemInfoLocal(mysqlServerServiceName string,
	monitorSystemSystemType int, monitorSystemHostIP string, monitorSystemPortNumSlow int) ([]query.Query, error) {
	monitorRepo, err := q.getMonitorRepo(monitorSystemSystemType, monitorSystemHostIP, monitorSystemPortNumSlow)
	if err != nil {
		return nil, err
	}
	defer func() {
		closeErr := monitorRepo.Close()
		if closeErr != nil {
			log.Errorf("query Querier.getByServiceNameAndMonitorSystemInfoLocal(): close monitor repo failed. err: \n%+v", closeErr)
		}
	}()

	return monitorRepo.GetByServiceNames([]string{mysqlServerServiceName})
}

// GetByDBID get queries by db id
func (q *Querier) GetByDBID(dbID int) ([]query.Query, error) {
	dbService := metadata.NewDBServiceWithDefault()
	err := dbService.GetByID(dbID)
	if err != nil {
		return nil, err
	}
	db := dbService.GetDBs()[constant.ZeroInt]
	mysqlServers, err := db.GetMySQLServers()
	if err != nil {
		return nil, err
	}

	var queries []query.Query
	for _, mysqlServer := range mysqlServers {
		monitorSystem, err := mysqlServer.GetMonitorSystem()
		if err != nil {
			return nil, err
		}
		qs, err := q.getByDBNameAndMySQLServerAndMonitorSystemInfoWithHTTP(db.GetDBName(),
			mysqlServer.GetHostIP(), mysqlServer.GetPortNum(), mysqlServer.GetServiceName(),
			monitorSystem.GetSystemType(), monitorSystem.GetHostIP(), monitorSystem.GetPortNumSlow())
		if err != nil {
			return nil, err
		}

		queries = append(queries, qs...)
	}

	return queries, nil
}

// GetByDBNameAndMySQLServerAndMonitorSystemInfo gets queries by db name and mysql server and monitor system info
func (q *Querier) GetByDBNameAndMySQLServerAndMonitorSystemInfo(dbName string,
	mysqlServerHostIP string, mysqlServerPortNum int, mysqlServerServiceName string,
	monitorSystemSystemType int, monitorSystemHostIP string, monitorSystemPortNumSlow int) ([]query.Query, error) {
	return q.getByDBNameAndMySQLServerAndMonitorSystemInfoWithHTTP(dbName,
		mysqlServerHostIP, mysqlServerPortNum, mysqlServerServiceName,
		monitorSystemSystemType, monitorSystemHostIP, monitorSystemPortNumSlow)
}

// getByDBNameAndMySQLServerAndMonitorSystemInfoWithHTTP gets queries by db name and mysql server and monitor system info
func (q *Querier) getByDBNameAndMySQLServerAndMonitorSystemInfoWithHTTP(dbName string,
	mysqlServerHostIP string, mysqlServerPortNum int, mysqlServerServiceName string,
	monitorSystemSystemType int, monitorSystemHostIP string, monitorSystemPortNumSlow int) ([]query.Query, error) {
	if viper.GetString(config.RoleKey) == config.ServerRole {
		// get mysql server
		mysqlService := metadata.NewMySQLServerServiceWithDefault()
		err := mysqlService.GetByHostInfo(mysqlServerHostIP, mysqlServerPortNum)
		if err != nil {
			return nil, err
		}
		// check accessibility
		deploymentService := deployment.NewServiceWithDefault(mysqlService.GetMySQLServers()[constant.ZeroInt])
		if !deploymentService.CheckAccessibility() {
			// prepare data
			data := []byte(fmt.Sprintf(utilquery.DBNameAndMySQLServerAndMonitorSystemInfoHTTPBodyTemplate,
				dbName, mysqlServerHostIP, mysqlServerPortNum, mysqlServerServiceName,
				monitorSystemSystemType, monitorSystemHostIP, monitorSystemPortNumSlow,
				q.getConfig().GetStartTime().Format(constant.TimeLayoutSecond), q.getConfig().GetEndTime().Format(constant.TimeLayoutSecond),
				q.getConfig().GetLimit(), q.getConfig().GetOffset(), q.getConfig().GetLoginName()))
			// send request
			respBody, err := deploymentService.PostDAS(utilquery.GetByDBNameAndMySQLServerAndMonitorSystemInfoPath, data)
			if err != nil {
				return nil, err
			}
			// unmarshal
			var result map[string][]*Query

			err = json.Unmarshal(respBody, &result)
			if err != nil {
				return nil, err
			}
			// prepare result
			var queries []query.Query
			for _, q := range result[queryQueriesJSON] {
				queries = append(queries, q)
			}

			return queries, nil
		}
	}

	return q.getByDBNameAndServiceNameAndMonitorSystemInfoLocal(dbName, mysqlServerServiceName, monitorSystemSystemType, monitorSystemHostIP, monitorSystemPortNumSlow)
}

func (q *Querier) getByDBNameAndServiceNameAndMonitorSystemInfoLocal(dbName, mysqlServerServiceName string,
	monitorSystemSystemType int, monitorSystemHostIP string, monitorSystemPortNumSlow int) ([]query.Query, error) {
	monitorRepo, err := q.getMonitorRepo(monitorSystemSystemType, monitorSystemHostIP, monitorSystemPortNumSlow)
	if err != nil {
		return nil, err
	}
	defer func() {
		closeErr := monitorRepo.Close()
		if closeErr != nil {
			log.Errorf("query Querier.getByDBNameAndServiceNameAndMonitorSystemInfoLocal(): close monitor repo failed. err: \n%+v", closeErr)
		}
	}()

	return monitorRepo.GetByDBName(mysqlServerServiceName, dbName)
}

// GetByMySQLServerIDAndSQLID gets queries by mysql server id and sql id
func (q *Querier) GetByMySQLServerIDAndSQLID(mysqlServerID int, sqlID string) ([]query.Query, error) {
	mysqlServerService := metadata.NewMySQLServerServiceWithDefault()
	err := mysqlServerService.GetByID(mysqlServerID)
	if err != nil {
		return nil, err
	}
	mysqlServer := mysqlServerService.GetMySQLServers()[constant.ZeroInt]
	monitorSystem, err := mysqlServer.GetMonitorSystem()
	if err != nil {
		return nil, err
	}

	return q.getBySQLIDAndMySQLServerAndMonitorSystemInfoWithHTTP(
		sqlID, mysqlServer.GetHostIP(), mysqlServer.GetPortNum(), mysqlServer.GetServiceName(),
		monitorSystem.GetSystemType(), monitorSystem.GetHostIP(), monitorSystem.GetPortNumSlow())
}

// GetBySQLIDAndMySQLServerAndMonitorSystemInfo gets queries by sql id and mysql server and monitor system info
func (q *Querier) GetBySQLIDAndMySQLServerAndMonitorSystemInfo(sqlID string,
	mysqlServerHostIP string, mysqlServerPortNum int, mysqlServerServiceName string,
	monitorSystemSystemType int, monitorSystemHostIP string, monitorSystemPortNumSlow int) ([]query.Query, error) {
	return q.getBySQLIDAndMySQLServerAndMonitorSystemInfoWithHTTP(
		sqlID, mysqlServerHostIP, mysqlServerPortNum, mysqlServerServiceName,
		monitorSystemSystemType, monitorSystemHostIP, monitorSystemPortNumSlow)
}

// getBySQLIDAndMySQLServerAndMonitorSystemInfoWithHTTP gets queries by sql id and mysql server and monitor system info
func (q *Querier) getBySQLIDAndMySQLServerAndMonitorSystemInfoWithHTTP(sqlID string,
	mysqlServerHostIP string, mysqlServerPortNum int, mysqlServerServiceName string,
	monitorSystemSystemType int, monitorSystemHostIP string, monitorSystemPortNumSlow int) ([]query.Query, error) {
	if viper.GetString(config.RoleKey) == config.ServerRole {
		// get mysql server
		mysqlService := metadata.NewMySQLServerServiceWithDefault()
		err := mysqlService.GetByServiceName(mysqlServerServiceName)
		if err != nil {
			return nil, err
		}
		// check accessibility
		deploymentService := deployment.NewServiceWithDefault(mysqlService.GetMySQLServers()[constant.ZeroInt])
		if !deploymentService.CheckAccessibility() {
			// prepare data
			data := []byte(fmt.Sprintf(utilquery.SQLIDAndMySQLServerAndMonitorSystemInfoHTTPBodyTemplate,
				sqlID, mysqlServerHostIP, mysqlServerPortNum, mysqlServerServiceName,
				monitorSystemSystemType, monitorSystemHostIP, monitorSystemPortNumSlow,
				q.getConfig().GetStartTime().Format(constant.TimeLayoutSecond), q.getConfig().GetEndTime().Format(constant.TimeLayoutSecond),
				q.getConfig().GetLimit(), q.getConfig().GetOffset(), q.getConfig().GetLoginName()))
			// send request
			respBody, err := deploymentService.PostDAS(utilquery.GetBySQLIDAndMySQLServerAndMonitorSystemInfoPath, data)
			if err != nil {
				return nil, err
			}
			// unmarshal
			var result map[string][]*Query

			err = json.Unmarshal(respBody, &result)
			if err != nil {
				return nil, err
			}
			// prepare result
			var queries []query.Query
			for _, q := range result[queryQueriesJSON] {
				queries = append(queries, q)
			}

			return queries, nil
		}
	}

	result, err := q.getBySQLIDAndServiceNameAndMonitorSystemInfoLocal(sqlID, mysqlServerServiceName, monitorSystemSystemType, monitorSystemHostIP, monitorSystemPortNumSlow)
	if err != nil {
		return nil, err
	}

	return []query.Query{result}, nil
}

func (q *Querier) getBySQLIDAndServiceNameAndMonitorSystemInfoLocal(sqlID, mysqlServerServiceName string,
	monitorSystemSystemType int, monitorSystemHostIP string, monitorSystemPortNumSlow int) (query.Query, error) {
	monitorRepo, err := q.getMonitorRepo(monitorSystemSystemType, monitorSystemHostIP, monitorSystemPortNumSlow)
	if err != nil {
		return nil, err
	}
	defer func() {
		closeErr := monitorRepo.Close()
		if closeErr != nil {
			log.Errorf("query Querier.getBySQLIDAndServiceNameAndMonitorSystemInfoLocal(): close monitor repo failed. err: \n%+v", closeErr)
		}
	}()

	return monitorRepo.GetBySQLID(mysqlServerServiceName, sqlID)
}

func (q *Querier) getMonitorRepo(monitorSystemSystemType int, monitorSystemHostIP string, monitorSystemPortNumSlow int) (query.MonitorRepo, error) {
	var monitorRepo query.MonitorRepo

	addr := fmt.Sprintf("%s:%d", monitorSystemHostIP, monitorSystemPortNumSlow)
	switch monitorSystemSystemType {
	case 1:
		// pmm 1.x
		mysqlConn, err := mysql.NewConn(addr, pmmDBName, viper.GetString(config.DBMonitorMySQLUserKey), viper.GetString(config.DBMonitorMySQLPassKey))
		if err != nil {
			return nil, message.NewMessage(msgquery.ErrQueryCreateMonitorMysqlConnection, err, addr, viper.GetString(config.DBMonitorMySQLUserKey))
		}
		monitorRepo = NewMySQLRepo(q.getConfig(), mysqlConn)
	case 2:
		// pmm 2.x
		clickhouseConn, err := clickhouse.NewConnWithDefault(addr, pmmDBName, viper.GetString(config.DBMonitorClickhouseUserKey), viper.GetString(config.DBMonitorClickhousePassKey))
		if err != nil {
			return nil, message.NewMessage(msgquery.ErrQueryCreateMonitorClickhouseConnection, err, addr, viper.GetString(config.DBMonitorClickhouseUserKey))
		}
		monitorRepo = NewClickHouseRepo(q.getConfig(), clickhouseConn)
	default:
		return nil, message.NewMessage(msgquery.ErrQueryMonitorSystemSystemType, monitorSystemSystemType)
	}

	return monitorRepo, nil
}
